/**
 * Copyright (c) 2011 Tomi Paananen.
 *
 * Part of Quick Gestures engine library.
 */

// Own header
#include "simpleclassifier.h"

// qgengine includes
#include "gestureprocessor.h"
#include "qgcommon.h"
#include "qgenginepriv.h"
#include "qgutils.h"
#include "qgtrace.h"

using namespace QGEngine;


/*!
  \class SimpleClassifier
  \brief Simple gesture classifier implementation.
*/


/*!
  Constructor.
*/
SimpleClassifier::SimpleClassifier()
    : m_processor(0),
      m_recognitionThreshold(qRound(GestureScaleFactor * GestureScaleFactor / 40))
{
    m_processor = new GestureProcessor();

    DEBUG_INFO("Coefficient for static gesture comparison is"
               << StaticCoefficient);
}


/*!
  Destructor.
*/
SimpleClassifier::~SimpleClassifier()
{
    delete m_processor;
}


/*!
  From SimpleClassifierIf.
*/
int SimpleClassifier::compare(Gesture *gesture, Gesture *candidate)
{
    if (!gesture || !candidate) {
        DEBUG_INFO("Error: Was given a NULL pointer!");
        return recognitionThreshold() + 1;
    }

    int difference(0);

    if (candidate->attributes() ==
            (Gesture::FixedDirection | Gesture::FixedStartingPoint)) {
        difference = errorFixedInitPointAndDirection(gesture->points(),
                                                     candidate->points());
    }
    else if (candidate->attributes() == Gesture::FixedDirection) {
        difference = errorFixedDirection(gesture->points(),
                                         candidate->points());

        for (int i = 1; i < FixedDirectionIterationCount; ++i) {
            int currentDiff =
                    errorFixedDirection(gesture->points(),
                                        candidate->points(),
                                        qrand() % candidate->points().count());

            if (currentDiff < difference) {
                difference = currentDiff;
            }
        }
    }
    else {
        // Static i.e. no fixed starting point nor fixed direction.
        /*difference =
            errorStaticUsingLines(gesture->points(), candidate->points()) * 2 +
            errorStatic(gesture->points(), candidate->points());*/
        // For the diff calc method above a proper coefficient is around 0.75
        // when the point count is 40.

        difference = errorStatic(gesture->points(), candidate->points());
        difference = qRound(difference * StaticCoefficient);
    }

    return difference;
}


/*!
  From ClassifierIf.
*/
int SimpleClassifier::recognitionThreshold()
{
    return m_recognitionThreshold;
}


/*!
  Sets the recognition threshold value to \a threshold.
*/
void SimpleClassifier::setRecognitionThreshold(int threshold)
{
    DEBUG_INFO(threshold);
    m_recognitionThreshold = threshold;
}


/*!
  Returns the error between points \a gesturePoints and \a candidatePoints.
  This method should be used when the candidate gesture has a fixed initial
  point and fixed direction.
*/
int SimpleClassifier::errorFixedInitPointAndDirection(const PointList &gesturePoints,
                                                      const PointList &candidatePoints)
{
    qreal error(0);

    const int gesturePointCount(gesturePoints.count());
    const int candidatePointCount(candidatePoints.count());

    int j(0);
    bool exitLoop(false);

    for (int i = 0; i < gesturePointCount && j < candidatePointCount; ++i) {
        // Make sure that no break points are compared.
        if (gesturePoints[i] == BreakPoint) {
            continue;
        }

        while (candidatePoints[j] == BreakPoint) {
            if (j < candidatePointCount - 1) {
                ++j;
            }
            else {
                // The candidate gesture has no more points.
                exitLoop = true;
                break;
            }
        }

        if (exitLoop) {
            break;
        }

        error += QGUtils::euclideanDistance(gesturePoints[i],
                                            candidatePoints[j]);
        ++j;
    }

    return qRound(error);
}


/*!
  Returns the error between points \a gesturePoints and \a candidatePoints.
  This method should be used when the candidate gesture has no fixed initial
  point but has fixed direction.
*/
int SimpleClassifier::errorFixedDirection(const PointList &gesturePoints,
                                          const PointList &candidatePoints,
                                          int startingIndex /* = 0 */)
{
    int error(recognitionThreshold() + 1);

    const int gesturePointCount(gesturePoints.count());
    const int candidatePointCount(candidatePoints.count());

    if (!gesturePointCount || !candidatePointCount) {
        DEBUG_INFO("Error: No points!");
        return error;
    }

    if (startingIndex >= candidatePointCount) {
        startingIndex = 0;
    }

    // Get the candidate point and make sure it is not a break point.
    QPoint candidatePoint(candidatePoints.at(startingIndex));

    while (candidatePoint == BreakPoint) {
        ++startingIndex;

        if (startingIndex >= candidatePointCount) {
            startingIndex = 0;
        }

        candidatePoint = candidatePoints.at(startingIndex);
    }

    // Find the point of the gesture with the least error compared to the
    // candidate point.
    int gestureIndex(0);
    qreal minError = QGUtils::euclideanDistance(gesturePoints.at(0),
                                                candidatePoint);
    qreal currentError;

    for (int i = 1; i < gesturePointCount; ++i) {
        if (gesturePoints.at(i) == BreakPoint) {
            continue;
        }

        currentError = QGUtils::euclideanDistance(gesturePoints.at(i),
                                                  candidatePoint);

        if (currentError < minError) {
            gestureIndex = i;
            minError = currentError;
        }
    }

    error = qRound(minError);

    int candidatePointsUsed(1);
    int gesturePointsUsed(1);
    QPoint gesturePoint;
    bool exitLoop(false);

    // Start traversing the points.
    for (int i = startingIndex; candidatePointsUsed < candidatePointCount; ++i) {
        if (i >= candidatePointCount) {
            i = 0;
        }

        // Get a new candidate point.
        candidatePoint = candidatePoints.at(i);
        ++candidatePointsUsed;

        if (candidatePoint == BreakPoint) {
            continue;
        }

        // Get a new gesture point.
        gesturePoint = BreakPoint;

        while (gesturePoint == BreakPoint) {
            if (gesturePointsUsed >= gesturePointCount) {
                // Used all gesture points.
                //DEBUG_INFO("All gesture points used, exiting loop...");
                exitLoop = true;
                break;
            }

            ++gestureIndex;

            if (gestureIndex >= gesturePointCount) {
                gestureIndex = 0;
            }

            gesturePoint = gesturePoints.at(gestureIndex);
            ++gesturePointsUsed;
        }

        if (exitLoop) {
            break;
        }

        error += qRound(QGUtils::euclideanDistance(gesturePoint, candidatePoint));
    }

    return error;
}


/*!
  Returns the error between the points \a gesturePoints and \a candidatePoints.
  This method should be used when the candidate gesture has neither a fixed
  initial point nor fixed direction.
*/
int SimpleClassifier::errorStatic(const PointList &gesturePoints,
                                  const PointList &candidatePoints)
{
    qreal error(0);

    const int gesturePointCount(gesturePoints.count());
    const int candidatePointCount(candidatePoints.count());

    QVector<int> gestureUnusedIndexes;
    QVector<int> candidateUnusedIndexes;

    const int maxIndex(
        (gesturePointCount > candidatePointCount) ?
            gesturePointCount : candidatePointCount);

    // Fill the index vectors and make sure that no break points get in there.
    for (int i = 0; i < maxIndex; ++i) {
        if (i < gesturePointCount && gesturePoints[i] != BreakPoint) {
            gestureUnusedIndexes.append(i);
        }

        if (i < candidatePointCount && candidatePoints[i] != BreakPoint) {
            candidateUnusedIndexes.append(i);
        }
    }

    QPoint gesturePoint;
    qreal pointError;
    qreal minError;
    int bestGestureIndex;
    int bestCandidateIndex;

    while (gestureUnusedIndexes.count() &&
           candidateUnusedIndexes.count()) {
        pointError = minError = MaxInt;
        bestGestureIndex = bestCandidateIndex = 0;

        for (int i = 0; i < gestureUnusedIndexes.count(); ++i) {
            gesturePoint = gesturePoints[gestureUnusedIndexes[i]];

            for (int j = 0; j < candidateUnusedIndexes.count(); ++j) {
                pointError = QGUtils::euclideanDistance(
                        gesturePoint,
                        candidatePoints[candidateUnusedIndexes[j]]);

                if (pointError < minError) {
                    minError = pointError;
                    bestGestureIndex = i;
                    bestCandidateIndex = j;
                }
            }
        }

        error += minError;

        gestureUnusedIndexes.remove(bestGestureIndex);
        candidateUnusedIndexes.remove(bestCandidateIndex);
    }

    return qRound(error);
}


/*!
    Returns the error between points \a gesturePoints and \a candidatePoints.

    The method transforms the gestures into lines before the comparison.

    This method should be used when the candidate gesture has neither a fixed
    initial point nor fixed direction.
*/
int SimpleClassifier::errorStaticUsingLines(const PointList &gesturePoints,
                                            const PointList &candidatePoints)
{
    qreal error(0);

    QList<QLine> gestureLines(m_processor->pointsToLines(gesturePoints));
    QList<QLine> candidateLines(m_processor->pointsToLines(candidatePoints));

    const int gestureLineCount(gestureLines.count());
    const int candidateLineCount(candidateLines.count());

    QVector<int> gestureUnusedIndexes;
    QVector<int> candidateUnusedIndexes;

    const int maxIndex(qMax(gestureLineCount, candidateLineCount));

    // Fill the index vectors.
    for (int i = 0; i < maxIndex; ++i) {
        if (i < gestureLineCount) {
            gestureUnusedIndexes.append(i);
        }

        if (i < candidateLineCount) {
            candidateUnusedIndexes.append(i);
        }
    }

    QLine gestureLine;
    QLine candidateLine;
    qreal currentError;
    qreal minError;
    int bestGestureIndex;
    int bestCandidateIndex;

    while (gestureUnusedIndexes.count() &&
           candidateUnusedIndexes.count()) {
        currentError = minError = MaxInt;
        bestGestureIndex = bestCandidateIndex = 0;

        for (int i = 0; i < gestureUnusedIndexes.count(); ++i) {
            gestureLine = gestureLines[gestureUnusedIndexes[0]];
            m_processor->normalizeLine(gestureLine);

            for (int j = 0; j < candidateUnusedIndexes.count(); ++j) {
                candidateLine = candidateLines[candidateUnusedIndexes[j]];
                m_processor->normalizeLine(candidateLine);

                currentError =
                        m_processor->compareLines(gestureLine, candidateLine);

                if (currentError < minError) {
                    minError = currentError;
                    bestGestureIndex = i;
                    bestCandidateIndex = j;
                }
            }
        }

        error += minError;

        gestureUnusedIndexes.remove(bestGestureIndex);
        candidateUnusedIndexes.remove(bestCandidateIndex);
    }

    return qRound(error);
}


// End of file.
